SpringMVC 的启动和执行过程详解

SpringMVC 启动过程

SpringMVC 启动的过程大致可以分为三个大的阶段:应用容器的初始化、Spring 父子容器的加载、SpringMVC 组件注册。


一:应用容器的初始化

Tomcat启动与SPI发现

在全注解模式下,启动不再依赖 web.xml。

  • Servlet 容器启动:当你运行 catalina.sh run,Tomcat 开始加载你的 WAR 包。
  • SPI 机制扫描:Spring 的 spring-web 包下有一个文件 META-INF/services/jakarta.servlet.ServletContainerInitializer,里面指向了 SpringServletContainerInitializer
  • 寻找初始化器:Tomcat 会调用这个类,它会扫描你项目中所有实现了 WebApplicationInitializer 接口的类。

还记得之前我们写的 WebInit 吗?我们正是通过这个类,将原来的 web.xml 完全替代。在这类中我们注册 RootConfig、WebMvcConfig,Filters、DispatcherServlet 等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 全注解开发,告别 web.xml 的核心启动类
*/
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

// 指定父容器配置类 (替代 applicationContext.xml)
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { RootConfig.class };
}

// 指定子容器配置类 (替代 spring-mvc.xml)
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { WebMvcConfig.class };
}

// 注册过滤器 (替代 web.xml 中的 Filter)
@Override
protected Filter[] getServletFilters() {
// 1. 创建编码过滤器
CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
encodingFilter.setEncoding("UTF-8");
encodingFilter.setForceResponseEncoding(true);

// 2. 创建 REST 请求方法支持过滤器
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();

// 返回数组的顺序即为 Filter 执行的顺序(必须把 encodingFilter 放在第一位,确保请求在被处理前先设置编码)
return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
}

// 指定 DispatcherServlet 的映射路径
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}

// 5. 其他自定义注册
@Override
protected void customizeRegistration(jakarta.servlet.ServletRegistration.Dynamic registration) {
// 如果没有找到映射,抛出异常而不是交给容器处理
registration.setInitParameter("throwExceptionIfNoHandlerFound", "true");
// 这里的参数等价于你在 Servlet 章节中学习的 @MultipartConfig
registration.setMultipartConfig(new jakarta.servlet.MultipartConfigElement("", 2097152, 4194304, 0)); // 2MB, 4MB, 阈值 0
//
super.customizeRegistration(registration);
}
}


SPI机制原理和案例

Spring MVC 之所以能实现 “零 XML 启动”,全靠 Java 原生的 SPI 机制。SPI 实际上是一种服务发现机制。它将 “接口定义” 和 “具体实现” 解耦,由第三方将插件式的代码注入到主程序中。它的核心逻辑只有三步:

  • 定义标准接口:主程序提供一个接口。
  • 编写注册实现类:实现在自己的项目路径 META-INF/services/ 下创建一个以 “接口全类名” 命名的文件,内容是 “实现类的全类名”。
  • 动态加载我们的实现:主程序使用 java.util.ServiceLoader 动态扫描并实例化这些类。

比如我们的系统需要支持多种翻译引擎,我们作为主程序来定义接口,不同的第三方可以有它们自己的实现,只需要通过 “接口”,就可以无缝插入到我们的主程序中来。

第一步:定义接口 (Service Interface)

1
2
3
public interface Translator {
String translate(String text);
}

第二步:编写具体实现 (Service Provider)

1
2
3
4
5
6
public class BaiduTranslator implements Translator {
@Override
public String translate(String text) {
return "Baidu: " + text + " -> 翻译完成";
}
}

第三步:关键——配置 SPI 文件

在项目的资源目录 src/main/resources 下手动创建文件夹和文件:

1
2
3
目录:META-INF/services/
文件名:com.demo.spi.Translator(必须是接口的全类名)
文件内容:com.demo.spi.impl.BaiduTranslator(实现类的全类名)

第四步:测试加载 (ServiceLoader)

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// ServiceLoader 是 SPI 的灵魂
ServiceLoader<Translator> translators = ServiceLoader.load(Translator.class);

for (Translator t : translators) {
System.out.println("加载到实现类:" + t.getClass().getName());
System.out.println(t.translate("Hello SPI"));
}
}

Tomcat(或任何 Servlet 容器)在启动时会寻找 jakarta.servlet.ServletContainerInitializer 的实现。

  • Spring-web 包里预设了 META-INF/services/jakarta.servlet.ServletContainerInitializer。
  • 内容指向了 Spring 的 SpringServletContainerInitializer。
  • 这个类会寻找所有的 WebApplicationInitializer。于是我们的 WebInit 就这样被 Spring 发现并运行了!

你可能会觉得这和策略模式很像。区别在于策略模式需要在代码里手动 new 一个具体的策略对象传给上下文。而程序根本不知道有哪些实现类,只有在运行时扫描 classpath 才知道,SPI 才是真正的 “插件化”。

Java 原生 SPI 有个缺点:它会一次性实例化所有实现类,且不支持依赖注入。这也是为什么 Dubbo 和 Spring 都对其进行了改造(例如 Spring 的 SpringFactoriesLoader)。


二:Spring 父子容器的加载

这是 Spring MVC 最精妙的 “双容器” 设计。为什么要有父子容器呢?主要是为了分层解耦,防止 Web 层的配置污染业务层,并且如果你有两个 DispatcherServlet(例如一个处理 API,一个处理后台管理),它们可以共享同一个父容器中的数据库连接,而各自拥有独立的拦截器和视图解析器。当 Tomcat 通过 SPI 机制找到并运行 Spring 的 SpringServletContainerInitializer 时,它会调用所有 WebApplicationInitializer 实现类的 onStartup(ServletContext) 方法。请记住,一切都是从这个方法开始的!

Spring MVC 源码级启动调用链
1.1 宿主环境启动 (Tomcat)
Tomcat StandardContext.startInternal() ➔ 扫描所有 jar 包下的 META-INF/services/jakarta.servlet.ServletContainerInitializer
1.2 Spring SPI 激活 (Spring-Web)
SpringServletContainerInitializer.onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext ctx) ➔ 获取到我们定义的 WebInit.class,循环调用其 onStartup(ctx)
2. 注册组件 (AbstractDispatcherServletInitializer) 👈
[A] 调用 registerContextLoaderListener(ctx)
createRootApplicationContext() : 实例化父容器
ctx.addListener(new ContextLoaderListener(rootContext)) : 挂载监听器
[B] 调用 registerDispatcherServlet(ctx)
createServletApplicationContext() : 实例化子容器
new DispatcherServlet(childContext) : 将子容器塞进 Servlet
3. 容器联姻与刷新 (DispatcherServlet 生命周期)
DispatcherServlet.init() ➔ FrameworkServlet.initServletBean()
核心代码节点:initWebApplicationContext()
  1. 通过 WebApplicationContextUtils 从 ServletContext 拿到父容器
  2. wac.setParent(rootContext) : ★父子引用正式建立★
  3. configureAndRefreshWebApplicationContext(wac) : 触发子容器 refresh()
  4. onRefresh(wac) : 开始初始化 HandlerMapping、ViewResolver 等

三:SpringMVC 组件注册

当容器准备好后,DispatcherServlet 开始它的初始化生命周期 init() 方法,如上图我们看到最终执行了 DispatcherServlet#onRefresh 这个钩子,这标志着 Spring MVC 从 “通用容器” 向 “Web 处理器” 的最后转变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}

/**
* 刷新 DispatcherServlet 使用的策略对象。
* 这些策略对象在 WebApplicationContext 中被定义。
*/
protected void initStrategies(ApplicationContext context) {
// 1. 文件上传解析器:探测是否定义了 multipartResolver。
// 如果你前端发的是 multipart/form-data,没有它,文件就转不成 MultipartFile。
initMultipartResolver(context);

// 2. 本地化解析器:处理国际化(i18n)。
// 它决定了如何从请求中识别用户的时区和语言,默认从 Accept-Language 头读取。
initLocaleResolver(context);

// 3. 主题解析器:处理更换皮肤/主题(现已逐渐被前端框架取代)。
// 根据请求决定加载哪套 CSS 或静态资源。
initThemeResolver(context);

// 4. [灵魂组件] 处理器映射器:寻找保存了 URL 和 Controller 方法对应关系的“导航地图”。
// 它会探测所有标注了 @RequestMapping 的方法。
// 如果你配置了 @EnableWebMvc 或 <mvc:annotation-driven />,Spring 会自动为你装配 RequestMappingHandlerMapping。
initHandlerMappings(context);

// 5. [灵魂组件] 处理器适配器:负责“调用”具体的 Controller 方法。
// 不同类型的 Controller(注解式、实现接口式)需要不同的适配器来处理参数注入。
initHandlerAdapters(context);

// 6. 异常解析器:探测处理异常的 Bean(如你之前写的 @ControllerAdvice)。
// 决定报错时是返回 JSON (R 对象) 还是跳转到 404/error 页面。
initHandlerExceptionResolvers(context);

// 7. 视图名翻译器:当 Controller 没有明确返回视图名时(返回 void)。
// 它负责自动根据 URL 生成一个视图名(如 /user 对应 user 视图)。
initRequestToViewNameTranslator(context);

// 8. 视图解析器:将逻辑视图名(如 "index")转为真实的物理路径(如 "/WEB-INF/templates/index.html")。
// 我们配置的 ThymeleafViewResolver 就是在这里被识别的。
initViewResolvers(context);

// 9. 闪传参数管理器:处理重定向时的数据传递。
// 比如:在 POST 提交成功后重定向到首页,并携带“操作成功”的提示消息。
initFlashMapManager(context);
}


SpringMVC 的调用过程

执行 doDispatch

DispatcherServlet 本质上还是一个 servlet,遵循 servlet 组件的调用规则。你会发现无论是 doGet、doPost、还是 service 方法,最终都会走到这个关键的 doDispatch 方法。

DispatcherServlet.doDispatch 核心逻辑
PREPARE 1. 检查多部分请求 (Multipart)
processedRequest = checkMultipart(request);

如果是文件上传,包装 Request;否则保持原样。

MAPPING 2. 获取处理器与适配器
mappedHandler = getHandler(processedRequest); // 🚩 找 Controller 和拦截器链
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 🚩 找对应的适配器

如果 mappedHandler 为空,直接触发 404 (noHandlerFound)。

EXECUTE 3. 拦截器与业务调用
if (!mappedHandler.applyPreHandle(processedRequest, response)) return; // 🚩 执行拦截器 preHandle
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // 🚩 真正执行 Controller

applyPreHandle 只要返回 false,请求就此止步。ha.handle 是我们的业务逻辑所在。

RESULT 4. 后置处理与响应渲染
mappedHandler.applyPostHandle(processedRequest, response, mv);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

即使业务报错,也会进入 processDispatchResult 进行异常页面或 JSON 的渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* DispatcherServlet 调度器的核心分发方法
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request; // 定义最终处理的请求对象(可能被包装)
HandlerExecutionChain mappedHandler = null; // 存储:1.目标Controller方法 2.拦截器链
boolean multipartRequestParsed = false; // 标记:是否为文件上传请求

// 1. 【异步处理准备】:获取异步管理器(Servlet 3.0+ 特性)
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null; // 逻辑视图与数据的载体
Exception dispatchException = null; // 业务异常记录器

try {
// 2. 【文件上传检查】:判断请求头是否包含 multipart/form-data
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 3. 【查找处理器】:通过 HandlerMapping 寻找谁来处理这个 URL
// 🚩 此步会返回包含拦截器的 HandlerExecutionChain
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 如果没找到对应的 Controller,处理 404
noHandlerFound(processedRequest, response);
return;
}

// 4. 【查找适配器】:根据 Handler 类型(如方法或接口)找对应的适配器(HandlerAdapter)
// 🚩 适配器模式:隐藏不同 Controller 实现的调用差异
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 5. 【浏览器缓存支持】:针对 GET/HEAD 请求检查 Last-Modified
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
// 若内容未变化,直接返回 304 状态码,节省带宽
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

// 6. 【拦截器前置处理】:顺序执行所有拦截器的 preHandle 方法
// 🚩 只要有一个拦截器返回 false,请求流程到此结束
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 7. 【核心业务调用】:适配器真正去调用 Controller 中的方法
// 🚩 此时会执行:参数解析、数据绑定、JSR303 校验、方法执行
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

// 8. 【异步跳出】:如果是异步处理,则直接返回,由另一个线程继续执行
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

// 9. 【视图容错】:如果 Controller 没返回视图名,根据 URL 猜一个视图名
applyDefaultViewName(processedRequest, mv);

// 10. 【拦截器后置处理】:逆序执行拦截器的 postHandle 方法
// 🚩 注意:如果 handle 抛出异常,这一步会被跳过
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
// 捕获业务逻辑执行中的异常
dispatchException = ex;
}
catch (Throwable err) {
// 捕获更严重的运行时错误,并包装成 ServletException
dispatchException = new ServletException("Handler dispatch failed: " + err, err);
}

// 11. 【结果分发处理】:处理 handle 产生的结果
// 🚩 关键:这里会调用视图解析器(ViewResolver)渲染页面,
// 或者调用 HandlerExceptionResolver 处理刚才捕获的 dispatchException
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
// 如果渲染过程(Step 11)也报错了,强制触发 afterCompletion
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new ServletException("Handler processing failed: " + err, err));
}
finally {
// 12. 【收尾清理】:
if (asyncManager.isConcurrentHandlingStarted()) {
// 异步处理的回调
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// 13. 【文件资源清理】:如果是上传请求,删除产生的临时文件
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}
}


拦截器 interceptors

还有以下三段方法来自于 HandlerExecutionChain 类,它们完美诠释了责任链模式在 Spring MVC 中的应用。最精妙的地方在于,这三个方法共同构建了一个 “剥洋葱” 式的执行结构:正序进入,逆序退出。

applyPreHandle:正序拦截(守门员)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 这是在 Controller 执行之前被调用的。它就像一道道关卡,决定请求是否能继续前进。
*/
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. 正序遍历拦截器列表:按照你在 MvcConfig 中注册的顺序执行
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);

// 2. 调用当前拦截器的 preHandle
if (!interceptor.preHandle(request, response, this.handler)) {
// 🚩 【关键点】:只要有一个返回 false (表示拦截)
// 立即触发 triggerAfterCompletion,清理已经通过的拦截器的资源
triggerAfterCompletion(request, response, null);
return false; // 中断流程,不再向下执行,也不执行 Controller
}

// 3. 记录当前成功执行到的拦截器索引
// 这是为了之后 triggerAfterCompletion 能知道从哪里开始“逆序”清理
this.interceptorIndex = i;
}
// 所有拦截器都放行,返回 true
return true;
}

applyPostHandle:逆序执行(加工员)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 执行已注册拦截器的 postHandle 方法
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

// 1. 【核心细节】:逆序遍历 (size-1 -> 0)
// 逻辑上:最后进入的拦截器,最先进行后置处理。
// 这就像剥洋葱,最里面那层先包回去。
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
// 2. 调用 postHandle。此时可以修改 ModelAndView 或向模型中追加数据
interceptor.postHandle(request, response, this.handler, mv);
}
}

triggerAfterCompletion:逆序清理(清洁工)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 触发拦截器的 afterCompletion 回调。
* 仅针对那些 preHandle 返回了 true 的拦截器。
*/
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
// 1. 【核心细节】:从 interceptorIndex 开始逆序向前执行
// 为什么?因为如果第3个拦截器拦截了,1和2执行过 preHandle,
// 那么只需要清理 2 和 1,不能清理 3 之后的(因为没执行过)。
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
// 2. 执行最终的回调,通常用于:
// - 资源清理(如 ThreadLocal.remove())
// - 性能统计(记录结束时间)
// - 异常记录(此处能拿到抛出的异常 ex 对象)
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
// 🚩 即使清理代码报错,也只是记个日志,不能影响主流程的结束
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}

为什么 afterCompletion 也要逆序呢?想象一下我们系统中拦截器的嵌套关系:

  1. 权限拦截器 (最外层)
  2. 日志拦截器 (中层)
  3. 事务拦截器 (最内层) -> Controller
  • 正序进入:权限检查 -> 开启日志 -> 开启事务。
  • 逆序退出:提交事务 -> 结束日志 -> 释放权限。

这种栈(Stack)式的结构保证了资源释放的安全性。比如,日志拦截器需要记录事务的结果,那么事务必须在日志结束前先提交。